package org.hibernate.cfg.reveng;

import java.beans.Introspector;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.ForeignKey;
import org.hibernate.mapping.PrimaryKey;
import org.hibernate.mapping.Table;
import org.hibernate.util.StringHelper;

/**
 * 默认反转策略实现
 * 
 * @author iPan
 * @version 2013-12-14
 */
public class DefaultReverseEngineeringStrategy implements ReverseEngineeringStrategy {

	static final private Log log = LogFactory.getLog(DefaultReverseEngineeringStrategy.class);

	private static Set AUTO_OPTIMISTICLOCK_COLUMNS;
	protected ReverseEngineeringSettings settings = new ReverseEngineeringSettings(this);
	protected ReverseEngineeringRuntimeInfo runtimeInfo;
	/** 生成实体类需要忽视的前缀 */
	private String[] ignorePrefix = new String[0];

	static {
		AUTO_OPTIMISTICLOCK_COLUMNS = new HashSet();
		AUTO_OPTIMISTICLOCK_COLUMNS.add("version");
		AUTO_OPTIMISTICLOCK_COLUMNS.add("timestamp");
	}

	public String columnToPropertyName(TableIdentifier table, String columnName) {
		String decapitalize = Introspector.decapitalize(toUpperCamelCase(columnName));
		return keywordCheck(decapitalize);
	}

	private String keywordCheck(String possibleKeyword) {
		if (ReverseEngineeringStrategyUtil.isReservedJavaKeyword(possibleKeyword)) {
			possibleKeyword = possibleKeyword + "_";
		}
		return possibleKeyword;
	}

	protected String toUpperCamelCase(String s) {
		return ReverseEngineeringStrategyUtil.toUpperCamelCase(s);
	}

	/**
	 * Does some crude english pluralization TODO: are the from/to names correct?
	 */
	public String foreignKeyToCollectionName(String keyname, TableIdentifier fromTable, List fromColumns,
			TableIdentifier referencedTable, List referencedColumns, boolean uniqueReference) {
		String propertyName = Introspector.decapitalize(StringHelper.unqualify(getRoot().tableToClassName(fromTable)));
		propertyName = pluralize(propertyName);

		if (!uniqueReference) {
			if (fromColumns != null && fromColumns.size() == 1) {
				String columnName = ((Column) fromColumns.get(0)).getName();
				propertyName = propertyName + "For" + toUpperCamelCase(columnName);
			} else { // composite key or no columns at all safeguard
				propertyName = propertyName + "For" + toUpperCamelCase(keyname);
			}
		}
		return propertyName;
	}

	protected String pluralize(String singular) {
		return ReverseEngineeringStrategyUtil.simplePluralize(singular);
	}

	public String foreignKeyToInverseEntityName(String keyname, TableIdentifier fromTable, List fromColumnNames,
			TableIdentifier referencedTable, List referencedColumnNames, boolean uniqueReference) {
		return foreignKeyToEntityName(keyname, fromTable, fromColumnNames, referencedTable, referencedColumnNames,
				uniqueReference);
	}

	public String foreignKeyToEntityName(String keyname, TableIdentifier fromTable, List fromColumnNames,
			TableIdentifier referencedTable, List referencedColumnNames, boolean uniqueReference) {
		String propertyName = Introspector.decapitalize(StringHelper.unqualify(getRoot().tableToClassName(referencedTable)));

		if (!uniqueReference) {
			if (fromColumnNames != null && fromColumnNames.size() == 1) {
				String columnName = ((Column) fromColumnNames.get(0)).getName();
				propertyName = propertyName + "By" + toUpperCamelCase(columnName);
			} else { // composite key or no columns at all safeguard
				propertyName = propertyName + "By" + toUpperCamelCase(keyname);
			}
		}

		return propertyName;
	}

	public String columnToHibernateTypeName(TableIdentifier table, String columnName, int sqlType, int length,
			int precision, int scale, boolean nullable, boolean generatedIdentifier) {
		String preferredHibernateType = JDBCToHibernateTypeHelper.getPreferredHibernateType(sqlType, length, precision,
				scale, nullable, generatedIdentifier);

		String location = "<no info>";
		if (log.isDebugEnabled()) {
			String info = " t:" + JDBCToHibernateTypeHelper.getJDBCTypeName(sqlType) + " l:" + length + " p:" + precision
					+ " s:" + scale + " n:" + nullable + " id:" + generatedIdentifier;
			if (table != null) {
				location = Table.qualify(table.getCatalog(), table.getSchema(), table.getName()) + "." + columnName + info;
			} else {

				location += " Column: " + columnName + info;
			}
		}
		if (preferredHibernateType == null) {
			log.debug("No default type found for [" + location + "] falling back to [serializable]");
			return "serializable";
		} else {
			log.debug("Default type found for [" + location + "] to [" + preferredHibernateType + "]");
			return preferredHibernateType;
		}
	}

	public boolean excludeTable(TableIdentifier ti) {
		return false;
	}

	public boolean excludeColumn(TableIdentifier identifier, String columnName) {
		return false;
	}

	public String tableToClassName(TableIdentifier tableIdentifier) {
		String pkgName = settings.getDefaultPackageName();
		String className = toUpperCamelCase(tableIdentifier.getName());
		if (pkgName.length() > 0) {
			className = StringHelper.qualify(pkgName, className);
		}
		return ignorePrefix(className);
	}

	public List getForeignKeys(TableIdentifier referencedTable) {
		return Collections.EMPTY_LIST;
	}

	public String getTableIdentifierStrategyName(TableIdentifier identifier) {
		return null;
	}

	public Properties getTableIdentifierProperties(TableIdentifier identifier) {
		return null;
	}

	public List getPrimaryKeyColumnNames(TableIdentifier identifier) {
		return null;
	}

	public String classNameToCompositeIdName(String className) {
		return className + "Id";
	}

	public void configure(ReverseEngineeringRuntimeInfo rti) {
		this.runtimeInfo = rti;
	}

	public void close() {

	}

	/** Return explicit which column name should be used for optimistic lock */
	public String getOptimisticLockColumnName(TableIdentifier identifier) {
		return null;
	}

	public boolean useColumnForOptimisticLock(TableIdentifier identifier, String column) {
		if (settings.getDetectOptimsticLock()) {
			return AUTO_OPTIMISTICLOCK_COLUMNS.contains(column.toLowerCase()) ? true : false;
		} else {
			return false;
		}
	}

	public List getSchemaSelections() {
		return null;
	}

	public String tableToIdentifierPropertyName(TableIdentifier tableIdentifier) {
		return null;
	}

	public String tableToCompositeIdName(TableIdentifier identifier) {
		return null;
	}

	public boolean excludeForeignKeyAsCollection(String keyname, TableIdentifier fromTable, List fromColumns,
			TableIdentifier referencedTable, List referencedColumns) {
		return !settings.createCollectionForForeignKey();
	}

	public boolean excludeForeignKeyAsManytoOne(String keyname, TableIdentifier fromTable, List fromColumns,
			TableIdentifier referencedTable, List referencedColumns) {
		return !settings.createManyToOneForForeignKey();
	}

	public boolean isForeignKeyCollectionInverse(String name, TableIdentifier foreignKeyTable, List columns,
			TableIdentifier foreignKeyReferencedTable, List referencedColumns) {
		Table fkTable = getRuntimeInfo().getTable(foreignKeyTable);
		if (fkTable == null) {
			return true; // we don't know better
		}

		if (isManyToManyTable(fkTable)) {
			// if the reference column is the first one then we are inverse.
			Column column = fkTable.getColumn(0);
			Column fkColumn = (Column) referencedColumns.get(0);
			if (fkColumn.equals(column)) {
				return true;
			} else {
				return false;
			}
		}
		return true;
	}

	public boolean isForeignKeyCollectionLazy(String name, TableIdentifier foreignKeyTable, List columns,
			TableIdentifier foreignKeyReferencedTable, List referencedColumns) {
		return true;
	}

	public void setSettings(ReverseEngineeringSettings settings) {
		this.settings = settings;
	}

	public ReverseEngineeringSettings getSettings() {
		return this.settings;
	}

	public boolean isOneToOne(ForeignKey foreignKey) {
		if (settings.getDetectOneToOne()) {
			// add support for non-PK associations
			List fkColumns = foreignKey.getColumns();
			List pkForeignTableColumns = null;

			if (foreignKey.getTable().hasPrimaryKey())
				pkForeignTableColumns = foreignKey.getTable().getPrimaryKey().getColumns();

			boolean equals = fkColumns != null && pkForeignTableColumns != null
					&& fkColumns.size() == pkForeignTableColumns.size();

			Iterator columns = foreignKey.getColumnIterator();
			while (equals && columns.hasNext()) {
				Column fkColumn = (Column) columns.next();
				equals = equals && pkForeignTableColumns.contains(fkColumn);
			}

			return equals;
		} else {
			return false;
		}
	}

	public boolean isManyToManyTable(Table table) {
		if (settings.getDetectManyToMany()) {

			// if the number of columns in the primary key is different
			// than the total number of columns then it can't be a middle table
			PrimaryKey pk = table.getPrimaryKey();
			if (pk == null || pk.getColumns().size() != table.getColumnSpan())
				return false;

			Iterator foreignKeyIterator = table.getForeignKeyIterator();
			List foreignKeys = new ArrayList();

			// if we have more than 2 fk, means we have more than 2 table
			// implied
			// in this table --> cannot be a simple many-to-many
			while (foreignKeyIterator.hasNext()) {
				ForeignKey fkey = (ForeignKey) foreignKeyIterator.next();
				foreignKeys.add(fkey);
				if (foreignKeys.size() > 2) {
					return false; // early exit if we have more than two fk.
				}
			}
			if (foreignKeys.size() != 2) {
				return false;
			}

			// tests that all columns are implied in the fks
			Set columns = new HashSet();
			Iterator columnIterator = table.getColumnIterator();
			while (columnIterator.hasNext()) {
				Column column = (Column) columnIterator.next();
				columns.add(column);
			}

			foreignKeyIterator = table.getForeignKeyIterator();
			while (!columns.isEmpty() && foreignKeyIterator.hasNext()) {
				ForeignKey element = (ForeignKey) foreignKeyIterator.next();
				columns.removeAll(element.getColumns());
			}
			// what if one of the columns is not the primary key?

			return columns.isEmpty();

		} else {
			return false;
		}
	}

	protected ReverseEngineeringStrategy getRoot() {
		return settings.getRootStrategy();
	}

	protected ReverseEngineeringRuntimeInfo getRuntimeInfo() {
		return runtimeInfo;
	}

	public String foreignKeyToManyToManyName(ForeignKey fromKey, TableIdentifier middleTable, ForeignKey toKey,
			boolean uniqueReference) {
		String propertyName = Introspector.decapitalize(StringHelper.unqualify(getRoot().tableToClassName(
				TableIdentifier.create(toKey.getReferencedTable()))));
		propertyName = pluralize(propertyName);

		if (!uniqueReference) {
			// TODO: maybe use the middleTable name here ?
			if (toKey.getColumns() != null && toKey.getColumns().size() == 1) {
				String columnName = ((Column) toKey.getColumns().get(0)).getName();
				propertyName = propertyName + "For" + toUpperCamelCase(columnName);
			} else { // composite key or no columns at all safeguard
				propertyName = propertyName + "For" + toUpperCamelCase(toKey.getName());
			}
		}
		return propertyName;
	}

	public Map tableToMetaAttributes(TableIdentifier tableIdentifier) {
		return null;
	}

	public Map columnToMetaAttributes(TableIdentifier identifier, String column) {
		return null;
	}

	public AssociationInfo foreignKeyToAssociationInfo(ForeignKey foreignKey) {
		return null;
	}

	public AssociationInfo foreignKeyToInverseAssociationInfo(ForeignKey foreignKey) {
		return null;
	}

	public String[] getIgnorePrefix() {
		return this.ignorePrefix;
	}

	public void setIgnorePrefix(String[] ignorePrefix) {
		if (ignorePrefix != null) {
			this.ignorePrefix = ignorePrefix;
		}
	}

	/**
	 * 忽视定义的前缀.
	 */
	protected String ignorePrefix(String delegateResult) {
		int lastDotIndex = delegateResult.lastIndexOf('.');
		String packageName = delegateResult.substring(0, lastDotIndex + 1);
		String tableName = delegateResult.substring(lastDotIndex + 1);
		String className = delegateResult.substring(lastDotIndex + getPrefixLength(tableName) + 1);
		String fullClassName = packageName + className;
		return fullClassName;
	}

	/**
	 * 在子类重载设定表名的前缀长度.
	 */
	protected int getPrefixLength(String tableName) {
		int prefixLenth = 0;
		for (int i = 0; i < this.ignorePrefix.length; ++i) {
			if (tableName.startsWith(this.ignorePrefix[i])) {
				prefixLenth = this.ignorePrefix[i].length();
				break;
			}
		}
		return prefixLenth;
	}
}
